-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Add Virtualizer to React Aria Components #6518
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This enables us to share a common KeyboardDelegate implementation between virtualized and non-virtualized collections
We now use the hook in Spectrum and pass the resulting columnWidths into the virtualizer layout
export interface AriaTableProps extends GridProps { | ||
/** The layout object for the table. Computes what content is visible and how to position and style them. */ | ||
layout?: Layout<Node<T>> | ||
layoutDelegate?: LayoutDelegate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically a breaking change but I'm hoping we can get away with it since virtualizer wasn't previously public?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i suspect it'll make some people unhappy anyways, but I think we'll get back into good graces by adding all the virtualizer/layoutDelegate support
otherwise, how impossible is it to support both?
or they could pass in their own copy of the old TableLayout? I guess the issue is the keyboard delegate stuff?
} | ||
|
||
if (nodes.length === 0) { | ||
if (nodes.length === 0 && this.enableEmptyState) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In RAC, the loading and empty states are handled in the component rather than in the layout. Ideally this would also the the case in RSP, but I haven't found a good way to implement that yet.
columnLayout: TableColumnLayout<T>, | ||
initialCollection: TableCollection<T> | ||
export interface TableLayoutOptions<T> extends ListLayoutOptions<T> { | ||
scrollContainer?: 'table' | 'body' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to handle a difference between RSP and RAC. In RSP, only the TableBody scrolls, whereas (so far) in RAC, the whole table scrolls and the column headers have position: sticky
. This affects the positioning of the body rows, which are relative to the body in RSP, and relative to the whole table in RAC.
} | ||
|
||
buildChild(node: Node<T>, x: number, y: number): LayoutNode { | ||
protected buildChild(node: Node<T>, x: number, y: number, parentKey: Key | null): LayoutNode { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This now takes a parentKey as a parameter rather than using the node's parentKey in order to properly handled TreeView's flattened collection. We want the parent LayoutInfo here, not the parent Node.
return this.getItem(keys[idx]); | ||
} | ||
|
||
getChildren(key: Key): Iterable<GridNode<T>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interoperability with RAC TableCollection.
newWidths.set(key, width); | ||
}); | ||
|
||
return newWidths; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps I am missing something here, but it seemed like we could simplify this algorithm a lot. We just need to lock in all of the columns to the left of the resizing one to their previous pixel values, and set the width of the resizing column. I don't think we need to run the sizing algorithm here at all, we can do that after the next render when the new widths are set. All the tests passed and it seemed to behave ok.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
at one point i think we needed to run the layout here so that the internal values for the columns was up to date
I think the changes you made that no longer needed since it'll actually update in render
// React calling removeChild on every item in the collection. | ||
document.isMounted = false; | ||
}; | ||
}, [document]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Improves performance a lot when unmounting a collection component with many items (e.g. 10,000 rows). Noticed when switching stories.
import {TableColumnResizeStateContext} from './Table'; | ||
import {useContext, useMemo} from 'react'; | ||
|
||
export class TableLayout<T> extends BaseTableLayout<T> implements LayoutOptionsDelegate<TableLayoutProps> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is using the BaseTableLayout, I found that it doesn't handle the "loader" type node that the load more spinner collection element due to
react-spectrum/packages/@react-stately/layout/src/TableLayout.ts
Lines 290 to 302 in 535e159
protected buildNode(node: GridNode<T>, x: number, y: number): LayoutNode { | |
switch (node.type) { | |
case 'headerrow': | |
return this.buildHeaderRow(node, x, y); | |
case 'item': | |
return this.buildRow(node, x, y); | |
case 'column': | |
case 'placeholder': | |
return this.buildColumn(node, x, y); | |
case 'cell': | |
return this.buildCell(node, x, y); | |
default: | |
throw new Error('Unknown node type ' + node.type); |
Or would the expectation be that the TableLayout be replaced/extended to handle new node types? Thinking from an angle where perhaps a section header/loader would need its calculated row height differentiated from a typical row
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If they were all items, how would we differentiate them elsewhere? Do we need to?
Otherwise I think it's fine to add more item types to the layout, would just work the same as item. I don't expect we'd be adding them that often.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now I've made the loader node trigger this.buildRow
as well. I do still wonder if there should be additional customization allowed for setting the height of the loader row independent of the row height but that can be handled in the future if requested.
…alizer # Conflicts: # packages/react-aria-components/test/GridList.test.js
function ResizableTableContainer(props: ResizableTableContainerProps, ref: ForwardedRef<HTMLDivElement>) { | ||
let objectRef = useObjectRef(ref); | ||
let containerRef = useObjectRef(ref); | ||
let tableRef = useRef<HTMLTableElement>(null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let tableRef = useRef<HTMLTableElement>(null); | |
let tableRef = useRef<HTMLTableElement | null>(null); |
// Add some margin around the loader to ensure that scrollbars don't flicker in and out. | ||
let rect = new Rect(40, Math.max(y, 40), (width || this.virtualizer.visibleRect.width) - 80, children.length === 0 ? this.virtualizer.visibleRect.height - 80 : 60); | ||
let loader = new LayoutInfo('loader', 'loader', rect); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just wanted to point out that this isLoading
block doesn't trigger for RAC tables since RAC TableBody doesn't take in loadingState
(which is good and expected) but if the user coincidentally passes loadingState='loading'
then this does trigger and messes up the layout by creating the loader layout info that we only actually use in RSP TableView.
I don't think this is really something to worry about probably but just wanted to call it out since this happens since we access the props set on the collection nodes in several places
import {TableColumnResizeStateContext} from './Table'; | ||
import {useContext, useMemo} from 'react'; | ||
|
||
export class TableLayout<T> extends BaseTableLayout<T> implements LayoutOptionsDelegate<TableLayoutProps> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another thing I just realized is that this exposes options like loaderHeight
that actually don't do anything due to how TableLayout implements its own buildCollection
. I've gotten rid of it from stately's TableLayout but I'll double check if there are any other options that need to be removed/aren't applicable to RAC TableLayout
Fixes virtualized select
RAC TableCollection now lists the head and body as its root nodes rather than the rows.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Verified that drag and drop works now and that behavior for RAC/RSP virtualized components work as expected. Only a couple of small questions but not blockers IMO
this.isLoading = invalidationContext.layoutOptions.isLoading; | ||
this.direction = invalidationContext.layoutOptions.direction; | ||
this.isLoading = invalidationContext.layoutOptions?.isLoading || false; | ||
this.direction = invalidationContext.layoutOptions?.direction || 'rtl'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this default rtl
? May not matter since the expectation is that the layoutOptions passed to Virtualizer/whatever provides the invalidationContext has the proper direction but just found it interesting that rtl
was the default if not provided
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo, good catch
|
||
rect.width = this.layoutInfos.get('header').rect.width; | ||
rect.width = this.layoutInfos.get(this.collection.head?.key ?? 'header').rect.width; | ||
rect.height = height + 1; // +1 for bottom border |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed that our layouts have some extra calculations built in like the above that adds 1 to the row height for the border. Will this be a bit strange for a RAC user who provides something like rowHeight: 25
to their TableLayout but then gets an actual row height of 26px?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm yeah. I am wondering if we split out a spectrum-specific layout from the generic one...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO it would ideal to split them. I'll add to the canvas to discuss this further Nevermind its already there, I'll leave some of my thoughts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At this point, happy to get in and we can follow up on the other ideas we had. Would be good if we can split any of that work amongst us
I'm testing out this branch and if I have dynamic heights for each element and scroll to the bottom it does not show all the elements and there is a large white gap at the bottom resulting in a blank viewport. I apologies if dynamic height isn't supported. This is for the VirtualizedListBox story, where I moved sections out of the component.
Also, will these virtualizer handle the case where an element could resize at any point? Such, as a row getting more data to render? |
It should work if you specify an |
I forgot to mention that I also changed |
Hmm your example is working for me locally... Any other steps to reproduce what you're seeing? |
Thanks for checking. The only other difference is I have Sorry for not creating a codebandbox for this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Happy for this to go in as is, we can discuss breaking out the RSP specific layout logic separately
…alizer # Conflicts: # packages/@react-aria/dnd/src/ListDropTargetDelegate.ts # packages/@react-aria/grid/src/GridKeyboardDelegate.ts # packages/@react-aria/table/src/useTable.ts # packages/@react-aria/virtualizer/src/ScrollView.tsx # packages/@react-spectrum/table/src/Resizer.tsx # packages/react-aria-components/src/Table.tsx # packages/react-aria-components/stories/GridList.stories.tsx # packages/react-aria-components/test/GridList.test.js
@rostero1 thanks, I see what you mean. I will look at it in a followup PR. |
## API Changes
unknown top level export { type: 'any' } @react-aria/autocompleteAriaSearchAutocompleteOptions AriaSearchAutocompleteOptions<T> {
inputRef: RefObject<HTMLInputElement | null>
keyboardDelegate?: KeyboardDelegate
+ layoutDelegate?: LayoutDelegate
listBoxRef: RefObject<HTMLElement | null>
popoverRef: RefObject<HTMLDivElement | null>
} it changed:
@react-aria/comboboxAriaComboBoxOptions AriaComboBoxOptions<T> {
buttonRef?: RefObject<Element | null>
inputRef: RefObject<HTMLInputElement | null>
keyboardDelegate?: KeyboardDelegate
+ layoutDelegate?: LayoutDelegate
listBoxRef: RefObject<HTMLElement | null>
popoverRef: RefObject<Element | null>
} it changed:
@react-aria/dndDragPreview ListDropTargetDelegate {
- constructor: (Collection<Node<unknown>>, RefObject<HTMLElement | null>, ListDropTargetDelegateOptions) => void
+ constructor: (Iterable<Node<unknown>>, RefObject<HTMLElement | null>, ListDropTargetDelegateOptions) => void
getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget
} @react-aria/gridGridKeyboardDelegatechanged by:
GridKeyboardDelegate<C extends GridCollection<T>, T> {
collection: GridCollection<T>
- constructor: (GridKeyboardDelegateOptions<T, GridCollection<T>>) => void
+ constructor: (GridKeyboardDelegateOptions<GridCollection<T>>) => void
getFirstKey: (Key, boolean) => void
getKeyAbove: (Key) => void
getKeyBelow: (Key) => void
getKeyForSearch: (string, Key) => void
getKeyPageAbove: (Key) => void
getKeyPageBelow: (Key) => void
getKeyRightOf: (Key) => void
getLastKey: (Key, boolean) => void
}
GridKeyboardDelegateOptions-GridKeyboardDelegateOptions<C, T> {
- collator?: Intl.Collator
- collection: C
- direction: Direction
- disabledBehavior?: DisabledBehavior
- disabledKeys: Set<Key>
- focusMode?: 'row' | 'cell'
- layout?: Layout<Node<T>>
- ref?: RefObject<HTMLElement | null>
-}
+ it changed:
undefined-
+GridKeyboardDelegateOptions<C> {
+ collator?: Intl.Collator
+ collection: C
+ direction: Direction
+ disabledBehavior?: DisabledBehavior
+ disabledKeys: Set<Key>
+ focusMode?: 'row' | 'cell'
+ layoutDelegate?: LayoutDelegate
+ ref?: RefObject<HTMLElement | null>
+} @react-aria/gridlistAriaGridListOptions AriaGridListOptions<T> {
disabledBehavior?: DisabledBehavior
isVirtualized?: boolean
keyboardDelegate?: KeyboardDelegate
keyboardNavigationBehavior?: 'arrow' | 'tab' = 'arrow'
+ layoutDelegate?: LayoutDelegate
linkBehavior?: 'action' | 'selection' | 'override' = 'action'
onAction?: (Key) => void
shouldFocusWrap?: boolean = false
} it changed:
@react-aria/listboxAriaListBoxProps AriaListBoxOptions<T> {
isVirtualized?: boolean
keyboardDelegate?: KeyboardDelegate
+ layoutDelegate?: LayoutDelegate
linkBehavior?: 'action' | 'selection' | 'override' = 'override'
shouldFocusOnHover?: boolean
shouldSelectOnPressUp?: boolean
shouldUseVirtualFocus?: boolean
@react-aria/selectionAriaSelectableListOptions AriaSelectableListOptions {
collection: Collection<Node<unknown>>
disabledKeys: Set<Key>
keyboardDelegate?: KeyboardDelegate
+ layoutDelegate?: LayoutDelegate
} it changed:
DOMLayoutDelegate-
+DOMLayoutDelegate {
+ constructor: (RefObject<HTMLElement>) => void
+ getContentSize: () => Size
+ getItemRect: (Key) => Rect | null
+ getVisibleRect: () => Rect
+} @react-aria/tableuseTablechanged by:
useTable<T> {
- props: AriaTableProps<T>
+ props: AriaTableProps
state: TableState<T> | TreeGridState<T>
ref: RefObject<HTMLElement | null>
returnVal: undefined
} AriaTableProps-AriaTableProps<T> {
- layout?: Layout<Node<T>>
-}
+ it changed:
undefined-
+AriaTableProps {
+ layoutDelegate?: LayoutDelegate
+} @react-aria/utilsmergeRefs mergeRefs<T> {
- refs: Array<ForwardedRef<T> | MutableRefObject<T>>
+ refs: Array<ForwardedRef<T> | MutableRefObject<T> | null | undefined>
returnVal: undefined
} @react-aria/virtualizeruseVirtualizerItem VirtualizerItem {
children: ReactNode
className?: string
- parent?: LayoutInfo
+ parent?: LayoutInfo | null
} layoutInfoToStyle ScrollView {
- children: ReactNode
+ children?: ReactNode
contentSize: Size
innerStyle?: CSSProperties
onScrollEnd?: () => void
onScrollStart?: () => void
scrollDirection?: 'horizontal' | 'vertical' | 'both'
sizeToFit?: 'width' | 'height'
}
setScrollLeft-
+useScrollView {
+ props: ScrollViewProps
+ ref: RefObject<HTMLElement | null>
+ returnVal: undefined
+} @react-spectrum/listboxListBox useListBoxLayout<T> {
- state: ListState<T>
returnVal: undefined
} @react-stately/layoutListLayoutchanged by:
ListLayout<T> {
- allowDisabledKeyFocus: boolean
- buildChild: (Node<T>, number, number) => LayoutNode
- buildCollection: () => Array<LayoutNode>
- buildHeader: (Node<T>, number, number) => LayoutNode
- buildItem: (Node<T>, number, number) => LayoutNode
- buildNode: (Node<T>, number, number) => LayoutNode
- buildSection: (Node<T>, number, number) => LayoutNode
- collection: Collection<Node<T>>
constructor: (ListLayoutOptions<T>) => void
- disabledKeys: Set<Key>
- ensureLayoutInfo: (Key) => void
getContentSize: () => void
getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget
- getFirstKey: () => Key | null
- getKeyAbove: (Key) => Key | null
- getKeyBelow: (Key) => Key | null
- getKeyForSearch: (string, Key) => Key | null
- getKeyPageAbove: (Key) => Key | null
- getKeyPageBelow: (Key) => Key | null
- getLastKey: () => Key | null
getLayoutInfo: (Key) => void
getVisibleLayoutInfos: (Rect) => void
- isLoading: boolean
- isValid: (Node<T>, number) => void
- isVisible: (LayoutNode, Rect) => void
- layoutIfNeeded: (Rect) => void
updateItemSize: (Key, Size) => void
- updateLayoutNode: (Key, LayoutInfo, LayoutInfo) => void
validate: (InvalidationContext<ListLayoutProps>) => void
} TableLayoutchanged by:
TableLayout<T> {
- addVisibleLayoutInfos: (Array<LayoutInfo>, LayoutNode, Rect) => void
- binarySearch: (Array<LayoutNode>, Point, 'x' | 'y') => void
- buildBody: (number) => LayoutNode
- buildCell: (GridNode<T>, number, number) => LayoutNode
- buildCollection: () => Array<LayoutNode>
- buildColumn: (GridNode<T>, number, number) => LayoutNode
- buildHeader: () => LayoutNode
- buildHeaderRow: (GridNode<T>, number, number) => LayoutNode
- buildNode: (GridNode<T>, number, number) => LayoutNode
- buildPersistedIndices: () => void
- buildRow: (GridNode<T>, number, number) => LayoutNode
collection: TableCollection<T>
- columnLayout: TableColumnLayout<T>
columnWidths: Map<Key, number>
constructor: (TableLayoutOptions<T>) => void
- controlledColumns: Map<Key, GridNode<unknown>>
- endResize: () => void
- getColumnMaxWidth: (Key) => number
- getColumnMinWidth: (Key) => number
- getColumnWidth: (Key) => number
getDropTargetFromPoint: (number, number, (DropTarget) => boolean) => DropTarget
- getEstimatedHeight: (GridNode<T>, number, number, number) => void
- getRenderedColumnWidth: (GridNode<T>) => void
- getResizerPosition: () => Key
getVisibleLayoutInfos: (Rect) => void
isLoading: any
lastCollection: TableCollection<T>
lastPersistedKeys: Set<Key>
persistedIndices: Map<Key, Array<number>>
- resizingColumn: Key | null
- setChildHeights: (Array<LayoutNode>, number) => void
- startResize: (Key) => void
+ scrollContainer: 'table' | 'body'
stickyColumnIndices: Array<number>
- uncontrolledColumns: Map<Key, GridNode<unknown>>
- uncontrolledWidths: Map<Key, ColumnSize>
- updateResizedColumns: (Key, number) => Map<Key, ColumnSize>
- wasLoading: any
+ validate: (InvalidationContext<TableLayoutProps>) => void
} ListLayoutProps-
+ListLayoutProps {
+ isLoading?: boolean
+} it changed:
TableLayoutOptions-
+TableLayoutOptions<T> {
+ scrollContainer?: 'table' | 'body'
+} it changed:
TableLayoutProps-
+TableLayoutProps {
+ columnWidths?: Map<Key, number>
+} it changed:
@react-stately/tableTableColumnResizeState TableColumnResizeState<T> {
+ columnWidths: Map<Key, number>
endResize: () => void
getColumnMaxWidth: (Key) => number
getColumnMinWidth: (Key) => number
getColumnWidth: (Key) => number
startResize: (Key) => void
tableState: TableState<T>
updateResizedColumns: (Key, number) => Map<Key, ColumnSize>
}
it changed:
TableCollection TableCollection<T> {
_size: number
at: (number) => void
body: GridNode<T>
columns: Array<GridNode<T>>
constructor: (Iterable<GridNode<T>>, ITableCollection<T>, GridCollectionOptions) => void
+ getChildren: (Key) => Iterable<GridNode<T>>
getFirstKey: () => void
getItem: (Key) => void
getKeyAfter: (Key) => void
getKeyBefore: (Key) => void
getLastKey: () => void
getTextValue: (Key) => string
headerRows: Array<GridNode<T>>
rowHeaderColumnKeys: Set<Key>
size: any
undefined: () => void
}
TableColumnLayout TableColumnLayout<T> {
buildColumnWidths: (number, TableCollection<T>, Map<Key, ColumnSize>) => void
columnMaxWidths: Map<Key, number>
columnMinWidths: Map<Key, number>
columnWidths: Map<Key, number>
constructor: (TableColumnLayoutOptions<T>) => void
getColumnMaxWidth: (Key) => number
getColumnMinWidth: (Key) => number
getColumnWidth: (Key) => number
getDefaultMinWidth: (GridNode<T>) => ColumnSize | null | undefined
getDefaultWidth: (GridNode<T>) => ColumnSize | null | undefined
getInitialUncontrolledWidths: (Map<Key, GridNode<T>>) => Map<Key, ColumnSize>
recombineColumns: (Array<GridNode<T>>, Map<Key, ColumnSize>, Map<Key, GridNode<T>>, Map<Key, GridNode<T>>) => Map<Key, ColumnSize>
- resizeColumnWidth: (number, TableCollection<T>, Map<Key, ColumnSize>, Map<Key, ColumnSize>, any, number) => Map<Key, ColumnSize>
+ resizeColumnWidth: (TableCollection<T>, Map<Key, ColumnSize>, Key, number) => Map<Key, ColumnSize>
splitColumnsIntoControlledAndUncontrolled: (Array<GridNode<T>>) => [Map<Key, GridNode<T>>, Map<Key, GridNode<T>>]
} @react-stately/virtualizerLayout Layout<O = any, T extends {}> {
getContentSize: () => Size
+ getItemRect: (Key) => Rect
getLayoutInfo: (Key) => LayoutInfo
getVisibleLayoutInfos: (Rect) => Array<LayoutInfo>
+ getVisibleRect: () => Rect
shouldInvalidate: (Rect, Rect) => boolean
updateItemSize: (Key, Size) => boolean
validate: (InvalidationContext<O>) => void
virtualizer: Virtualizer<{}, any, any>
|
Closes #5067
This PR implements a
Virtualizer
in React Aria Components, using the collection renderer system added in #5912.There are 3 parts which you can review commit by commit:
LayoutDelegate
interface, which enables us to share a common KeyboardDelegate implementation between virtualized and non-virtualized collections. Previously, ListLayout implemented the KeyboardDelegate interface itself, which was mostly duplicated with the DOM-based ListKeyboardDelegate. It also resulted in us needing to sync data into the layout such asdisabledKeys
. Now we can use ListKeyboardDelegate in both cases. It accepts aLayoutDelegate
, which provides the rectangles of item elements and the collection itself. There is a defaultDOMLayoutDelegate
implementation, and in addition, allLayout
classes automatically implement this interface as well.columnWidths
into the virtualizer layout vialayoutOptions
, rather than storing state in the layout. I've also managed to simplify the algorithm for resizing a column. This enables RAC to implement resizing the same way as RSP.Virtualizer
to React Aria Components. This is implemented as a CollectionRenderer, which has been refactored a bit. It now provides two components:CollectionRoot
renders the root items of a collection, andCollectionBranch
renders the children of a branch item. Virtualizer attaches scroll events to the providedscrollRef
at the root, determines which views need to be displayed, and renders those.The end result is that you can wrap any collection component in a
Virtualizer
, pass in alayout
as a prop, and the component will have virtualized scrolling. The rest of the API is the same as it is today.This means the layouts are also customizable. For example, you could pass a
GridLayout
here and the items would be arranged in a grid.Virtualizer
delegates to the layout for keyboard navigation and drag and drop automatically, and persists the focused key in the DOM. Individual components may also add additional ARIA attributes when virtualized, e.g.aria-setsize
andaria-posinset
. Otherwise, they are unaware of virtualization, which means you only pay the bundle size cost when needed, and you could even implement a custom CollectionRenderer to use a different virtualization library if you wanted.